#!/usr/bin/python2.4
#
# Copyright 2007 Dobromir Montauk. All Rights Reserved.

"""This is the main Dashomatic module that you're likely to use.

Currently depends on having Kid and pygooglechart installed.
"""

__author__ = """dmontauk@gmail.com (Dobromir Montauk)"""

import csv
import StringIO
import pygooglechart
import kid
kid.enable_import()


from dashomatic import BadDataError,BadChartError
from dashomatic import data_processor
import dashomatic.templates.default

class Dashboard(object):

  DEFAULT_CHART_WIDTH = 400
  DEFAULT_CHART_HEIGHT = 200
  DEFAULT_ROUND_AMOUNT = 2

  def __init__(self):
    """Initialize a Dashboard object.
    
    Args:
    Returns:
      A Dashboard object
    Throws:
    """
    self.charts = {}
    self.data = {}
    
  def AddData(self,name,data,headers=None,empty=""):
    """Add some data to the dashboard (as a Data instance)
    
    Args:
      name: string, label for the data
      data: list of dicts or list of lists. Can also be a Data instance.
      headers: list (optional). See Data object for details.
      empty: string (optional). See Data object for details.
    Returns:
      None
    Throws:
      BadDataError, if the error isn't in the right format
    """
    if isinstance(data,Data):
      self.data[name] = data
    else:
      self.data[name] = Data(data,headers,empty)
    # Prepare space for charts on this data
    self.charts[name] = {}
       
  def AddChart(self,data_source,header,chart):
    """Add a chart to the dashboard. 
    
    Also fills out some defaults, like the Title, Y-Range, X-Labels, etc if none
    are there.
    
    TODO(dmontauk): Right now, we have *no* ability to configure labels, range,
    etc. Our "smart" values probably won't work for everyone.
    
    Arg:
      header: the column name you want to use as the data source
      chart: either a pygooglechart instance or a dictionary we'll translate
             to pygooglechart
    Returns:
      None
    Throws:
      BadDataError,BadChartError
    """
    try:
      index = self.data[data_source].headers.index(header)
    except (KeyError,ValueError):
      raise BadDataError("The header %s in the data source %s doesn't exist." 
                          % (header,data_source))
    data = self.GetColumn(data_source,index)
    normalized_data = data_processor.NormalizeDataRange(data)
    if isinstance(chart,dict):
      chart = self.DictToChart(chart)
    chart.add_data(normalized_data)
    if not chart.title:
      chart.title = header
      
    labels = map(str,self.GetColumn(data_source,0))
    if labels:
      for i in range(1,len(labels)-1):
        if i != len(labels)/2:
          labels[i] = ""
      chart.set_axis_labels(pygooglechart.Axis.BOTTOM,labels)
    chart.set_axis_range(pygooglechart.Axis.LEFT,0,max(data))

    self.charts[data_source][header] = chart
    return chart.get_url() # Check if things worked?
        
  @classmethod
  def DictToChart(cls,d):
    """Takes a dictionary and transforms it to a pygooglechart object.
    
    Args:
      d: a dictionary with the following format:
        { "type": str,    # or any other supported type
          "width": int,
          "height": int,
          "legend": [str,str,...],   # one str per data set
          "colors": [str,str,...],   # one str per data set
          "title": str,
          
        }
    Returns:
      A pygooglechart instance.
    Throws:
      BadChartError if we can't parse the dict
    """
    if not d.has_key('type'):
      raise BadChartError("You must pass in a chart type.")
    height = d.get("height") or cls.DEFAULT_CHART_HEIGHT
    width = d.get("width") or cls.DEFAULT_CHART_WIDTH
    
    try:
      chart = getattr(pygooglechart,d.get('type'))(width,height)
      if d.has_key("legend"):
        chart.set_legend(d.get("legend"))
      if d.has_key("colors"):
        chart.set_colours(d.get('colors'))
      if d.has_key("title"):
        chart.set_title(d.get('title'))
      return chart
    except (AttributeError,AssertionError),e:
      if isinstance(e,AttributeError):
        raise BadChartError("No chart type '%s'." % d.get('type'))  
      else:
        raise BadChartError(e)  
    
  def GetColumn(self,data_source,index):
    """Returns a list of the "column" at index.
    
    Args:
      data_source: a string, labeling which data source we want.
      index: an int
    Returns:
      a list
    Throws:
      BadDataError
    """
    try:
      return self.data[data_source].GetColumn(index)
    except (KeyError,IndexError):
      raise BadDataError("Column %s in data source %s doesn't exist." % 
                          (index,data_source))

  def BuildDashboard(self,dashboard_title,template=None,css="default"):
    """Build the HTML of the dashboard using the provided template & css.
    
    Args:
      dashboard_title: The name the dashboard should have.
      template: the Kid template to use to build the dashboard.
      css: the CSS file to include in the template.
    Returns:
      Nothing      
    Throws:
      BadDataError
    """
    self.template = template
    if not template:
      self.template = dashomatic.templates.default.Template()
    self.template.dashboard_title = dashboard_title
    self.template.charts = self.charts
    #self.template.headers = self.headers
    self.template.data = self.data
    self.template.round_amount = self.DEFAULT_ROUND_AMOUNT
    self.html = self.template.serialize()
    return self.html
      
  def BuildHtmlDashboardToDir(self,directory):
    """Takes data and, optionally, charts and saves the HTML dashboard to the 
    specified directory.
    
    Args:
      directory: the directory where to save the dashboards to
      (others, see BuildHtmlDashboard)
    Returns:
      Nothing
    Throws:
      ???
    """
    dashboard = BuildHtmlDashboard(data,charts)
    raise Exception("Not finished")
    
  def BuildCsv(self,data_sources=None):
    """Returns a CSV of the requested data sources.
    
    Args:
      data_sources: a list of strings representing the data sources to dump
    Returns:
      A string (CSV)
    Throws:
      Nothing  
    """
    if not data_sources:
      data_sources = self.data.keys()
    result_csv = StringIO.StringIO()
    writer = csv.writer(result_csv)
    for source in data_sources:
      writer.writerow(self.data[source].headers)
      writer.writerows(self.data[source].data)
    return result_csv.getvalue()

class Data:
  """
  
  """
  def __init__(self,data,headers=None,empty=""):
    """Create a Data object.
    
    Args:
      data: a list of dicts or a list of lists. Each element is assumed to be a
            row in a table. 
      headers: list, the headers to use for this data (optional). If not passed
              in, we use the first row (keys, or values) as the headers.
      empty: string, the placeholder to put into missing cells
    Returns:
      Data object
    Throws:
      BadDataError
    """
    (self.headers, self.data) = self.ProcessRawData(data,headers,empty)
  
  @classmethod
  def ProcessRawData(cls,data,headers,empty):
    """We process raw lists of dicts (better) or lists of lists (worse) to get
    out the headers & actual data.
    
    Args:
      data: a list of dicts or list of lists.
      headers: the headers to use. If data contains dicts, these headers must
                appear in each dict. If data contains lists, we use these as the
                headers. If not passed in, we grab the headers from the dicts or
                the first list.
      empty: what to fill in if the data has different-sized lists.
    Returns:
      (headers, data)
    Raises:
      BadDataError if the data is incorrectly formatted.
    """
    if not data:
      raise BadDataError("We can't process no data.")
    if isinstance(data[0],dict):
      new_data = data_processor.ListOfDictsToListOfLists(data,headers,empty)
      headers = new_data.pop(0)
      return (headers,new_data)
    elif isinstance(data[0],(list,tuple)):
      new_data = data_processor.NormalizeListOfLists(data,empty=empty)
      if not headers:
        headers = new_data.pop(0)
      return (headers,new_data)
    raise BadDataError("Your data doesn't seem to contain dicts or lists. We "
                       "only accept [{},{},...] or [ [],[],[],...] .")

  def GetColumn(self,index):
    """Returns a list of the "column" at index.
    
    Args:
      index: an int
    Returns:
      a list
    Throws:
      IndexError if the index is out of bounds
    """
    return [data[index] for data in self.data]
